Skip to content

Conversation

evenkkl
Copy link
Member

@evenkkl evenkkl commented Oct 10, 2026

What this does

Adds server/: a GraphQL Yoga API on port 3001 that answers searchAlbums from the existing schema.graphql, backed by PostgreSQL through Drizzle. This is the core query from the brief: search on single words, case-insensitive, with filtering, sorting and paging done in SQL over the whole result set.

Only what the issue asks for is here. No album(id), no reviews, crate or mutations. Those come with their own issues.

How it meets the checkboxes

  • websearch_to_tsquery('simple', …) against the GIN-indexed search_vector. search_vector holds the album title plus artist names, built with to_tsvector('simple', …). 'simple' lowercases and does no stemming, so search is case-insensitive and word-based. EXPLAIN confirms the GIN index is used.
  • Facets computed with their own dimension removed. genreFacets ignores the selected genres but keeps the search term and decades, and decadeFacets does the same for decades. Selecting Rock still shows how many Pop or Jazz albums match.
  • Keyset cursors in search/search.ts, pure and unit-tested. A cursor holds the sort, the last row's sort value and its id. The next page asks for rows strictly after it, so no OFFSET. NULL release years sort last and page correctly. A cursor from a different sort is rejected. 12 Vitest tests.
  • Per-request DataLoaders in loaders/index.ts. Album.artists and Album.genres load the whole page in one query each. A page costs 6 SQL queries whether it has 3 albums or 50.

Files

Path Purpose
server/src/index.ts Starts Yoga, loads the root schema.graphql, new loaders per request
server/src/resolvers.ts searchAlbums (page, totalCount, both facets in parallel) and the Album relation fields
server/src/search/search.ts (+ .test.ts) WHERE clause, sort order and keyset cursors. No database access
server/src/loaders/index.ts DataLoaders for artists and genres
server/src/db/schema.ts, server/drizzle/ Catalog tables only (album, artist, genre, two join tables) and the GIN index
server/src/db/client.ts, migrate.ts, seed.ts Connection pool, migration runner, 16 sample albums
docs/VM-setup.md Run steps and the curl smoke suite the acceptance line points to

Root pnpm build now builds server/ too. The deploy job errors if server/ exists without server/dist/index.js.

Worth knowing

  • docs/VM-setup.md is back, but small. docs: #8 record the VM deployment in README and AGENTS.md #41 removed the full runbook. The acceptance line for this issue names that file, so it only holds the smoke suite and five run commands, not a runbook.
  • AVERAGE_RATING_DESC returns a clear error until reviews are stored.
  • The VM database was reset. The old prototype tables in crate were dropped (backup in ~evenkkl/crate-prototype-backup.sql). crate-api still runs Erik's hand-copied prototype, which now errors on search. Pointing the service at this code is a separate infra task.

What was tested

Check Result
pnpm format:check, lint, typecheck, test, build pass (12 new server tests)
Smoke suite against the built server and a local Postgres all 5 pass
Paging through every sort, 5 per page each album exactly once, NULL years last
Relevance paging across tied ranks no duplicates or gaps
SQL queries per page (3 vs 16 albums) 6 and 6
EXPLAIN of the search Bitmap Index Scan on album_search_vector_idx
Bad cursor, bad genre id, odd search input clear errors, no crash
Smoke suite on the VM (port 3002) fill in after running

Closes

Closes #6.

- search/search.ts: websearch_to_tsquery('simple', ...) against the
  GIN-indexed search_vector, facet filters that drop their own dimension,
  and keyset cursors. Pure and unit-tested.
- loaders/index.ts: per-request DataLoaders for Album.artists and
  Album.genres, so a page costs 6 queries whatever its size.
- Drizzle migration for the catalog tables and a 16-album seed.
- docs/VM-setup.md: run steps and the curl smoke suite.
- Root build now builds server/ too, since the deploy job expects
  server/dist/index.js once server/ exists.
@evenkkl evenkkl requested a review from a team as a code owner October 10, 2026 12:46
@evenkkl evenkkl linked an issue Oct 10, 2026 that may be closed by this pull request
4 tasks
erikhfj added a commit that referenced this pull request Oct 10, 2026
The workflow built server/ in the runner's own workspace and restarted
crate-api, but nothing ever put that build where the unit actually runs
it. crate-api.service has WorkingDirectory=~/T19-Project-2/server and
ExecStart=.../node dist/index.js, so a restart just brought back whatever
had been installed on the machine by hand. Reported by rachelks on #42.

The deploy step now:

1. checks out $GITHUB_SHA in ~/T19-Project-2, which is a git clone of
   main as of this issue rather than a hand copy
2. installs and builds server/ there
3. restarts crate-api and polls until /graphql answers
4. runs a smoke suite through the same Apache proxy the browser uses

Three things this fixes that were wrong before:

- The unit check is [ -f ... ], not `systemctl list-unit-files | grep`.
  grep -q closes the pipe at the first match, systemctl dies of SIGPIPE,
  and with pipefail the pipeline reports failure. The if took the else
  branch and skipped a service that was definitely there.

- The restart needs a NOPASSWD sudoers rule, because the runner has no
  TTY. Scoped to three systemctl commands.

- No sleep. Type=simple reports active before Node binds the port, and a
  measured 1850 ms startup made `sleep 2` produce a 503 through Apache.
  It polls instead.

Verified on the VM: ~/T19-Project-2 is a real clone at 0fbd54a, git fetch
works with the deploy key added for that machine, `checkout --force` leaves
.gitignore's .env untouched (it holds DATABASE_URL), and sudo is
passwordless for the three commands.

The skip guard for `server/` stays, because #42 has not merged yet. Once
it does, the next push to main will deploy the API end to end.

Co-Authored-By: opencode <noreply@opencode.ai>
erikhfj added a commit that referenced this pull request Oct 12, 2026
The workflow built server/ in the runner's own workspace and restarted
crate-api, but nothing ever put that build where the unit actually runs
it. crate-api.service has WorkingDirectory=~/T19-Project-2/server and
ExecStart=.../node dist/index.js, so a restart just brought back whatever
had been installed on the machine by hand. Reported by rachelks on #42.

The deploy step now:

1. checks out $GITHUB_SHA in ~/T19-Project-2, which is a git clone of
   main as of this issue rather than a hand copy
2. installs and builds server/ there
3. restarts crate-api and polls until /graphql answers
4. runs a smoke suite through the same Apache proxy the browser uses

Three things this fixes that were wrong before:

- The unit check is [ -f ... ], not `systemctl list-unit-files | grep`.
  grep -q closes the pipe at the first match, systemctl dies of SIGPIPE,
  and with pipefail the pipeline reports failure. The if took the else
  branch and skipped a service that was definitely there.

- The restart needs a NOPASSWD sudoers rule, because the runner has no
  TTY. Scoped to three systemctl commands.

- No sleep. Type=simple reports active before Node binds the port, and a
  measured 1850 ms startup made `sleep 2` produce a 503 through Apache.
  It polls instead.

Verified on the VM: ~/T19-Project-2 is a real clone at 0fbd54a, git fetch
works with the deploy key added for that machine, `checkout --force` leaves
.gitignore's .env untouched (it holds DATABASE_URL), and sudo is
passwordless for the three commands.

The skip guard for `server/` stays, because #42 has not merged yet. Once
it does, the next push to main will deploy the API end to end.

Co-Authored-By: opencode <noreply@opencode.ai>
- Validate cursor values against the sort, so a mismatched cursor gets
  "Invalid cursor" instead of a Postgres error or wrong page.
- Album.reviewCount returns 0 until reviews are stored.
- Genre filters combine with AND, as agreed in the group. Genre facet
  counts now include the selected genres, so "Pop (3)" with Rock ticked
  matches what ticking Pop returns. Decades stay OR.
- Smoke suite expectations and AI usage log updated.
Copy link
Member

@rachelks rachelks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good now! Both review comments have been addressed, and CI is passing. Approved!

@rachelks rachelks merged commit c71d971 into main Oct 13, 2026
2 checks passed
erikhfj added a commit that referenced this pull request Oct 15, 2026
The workflow built server/ in the runner's own workspace and restarted
crate-api, but nothing ever put that build where the unit actually runs
it. crate-api.service has WorkingDirectory=~/T19-Project-2/server and
ExecStart=.../node dist/index.js, so a restart just brought back whatever
had been installed on the machine by hand. Reported by rachelks on #42.

The deploy step now:

1. checks out $GITHUB_SHA in ~/T19-Project-2, which is a git clone of
   main as of this issue rather than a hand copy
2. installs and builds server/ there
3. restarts crate-api and polls until /graphql answers
4. runs a smoke suite through the same Apache proxy the browser uses

Three things this fixes that were wrong before:

- The unit check is [ -f ... ], not `systemctl list-unit-files | grep`.
  grep -q closes the pipe at the first match, systemctl dies of SIGPIPE,
  and with pipefail the pipeline reports failure. The if took the else
  branch and skipped a service that was definitely there.

- The restart needs a NOPASSWD sudoers rule, because the runner has no
  TTY. Scoped to three systemctl commands.

- No sleep. Type=simple reports active before Node binds the port, and a
  measured 1850 ms startup made `sleep 2` produce a 503 through Apache.
  It polls instead.

Verified on the VM: ~/T19-Project-2 is a real clone at 0fbd54a, git fetch
works with the deploy key added for that machine, `checkout --force` leaves
.gitignore's .env untouched (it holds DATABASE_URL), and sudo is
passwordless for the three commands.

The skip guard for `server/` stays, because #42 has not merged yet. Once
it does, the next push to main will deploy the API end to end.

Co-Authored-By: opencode <noreply@opencode.ai>
erikhfj added a commit that referenced this pull request Oct 15, 2026
…checks

Four points from rachelks on #45.

1. The searchAlbums smoke query omitted `sort`, which is `AlbumSort!` in
   AlbumSearchInput, so the query failed validation and the check could
   never pass. Now sends `sort: TITLE_ASC`.

2. The skip guards for crate-api.service and server/ now fail instead of
   exiting 0. #42 has merged so both are expected, and a deploy job that
   goes green without deploying anything is the wrong failure mode.

3. The body check parses the JSON rather than grepping for the word
   "errors". Grep would also fire on an album called "Errors", and it did
   not verify `data` was present at all. Now rejects a response with
   errors, with no data, or that is not JSON. Added a totalCount
   assertion as well, so the check is on the data and not only on its
   presence.

4. The python validators live in shell variables on one line. A
   multi-line `python3 -c` heredoc cannot be used inside a YAML block
   scalar: the block ends at the first unindented line and the workflow
   stops parsing. Learned by breaking it.

Also rebased onto main now that #42 and #43 have merged.

Validated the two python expressions against five payloads: a good
response, GraphQL errors, null data, a valid totalCount, and two bad
ones. Each rejected case prints what was wrong.

Co-Authored-By: opencode <noreply@opencode.ai>
erikhfj added a commit that referenced this pull request Oct 15, 2026
The workflow built server/ in the runner's own workspace and restarted
crate-api, but nothing ever put that build where the unit actually runs
it. crate-api.service has WorkingDirectory=~/T19-Project-2/server and
ExecStart=.../node dist/index.js, so a restart just brought back whatever
had been installed on the machine by hand. Reported by rachelks on #42.

The deploy step now:

1. checks out $GITHUB_SHA in ~/T19-Project-2, which is a git clone of
   main as of this issue rather than a hand copy
2. installs and builds server/ there
3. restarts crate-api and polls until /graphql answers
4. runs a smoke suite through the same Apache proxy the browser uses

Three things this fixes that were wrong before:

- The unit check is [ -f ... ], not `systemctl list-unit-files | grep`.
  grep -q closes the pipe at the first match, systemctl dies of SIGPIPE,
  and with pipefail the pipeline reports failure. The if took the else
  branch and skipped a service that was definitely there.

- The restart needs a NOPASSWD sudoers rule, because the runner has no
  TTY. Scoped to three systemctl commands.

- No sleep. Type=simple reports active before Node binds the port, and a
  measured 1850 ms startup made `sleep 2` produce a 503 through Apache.
  It polls instead.

Verified on the VM: ~/T19-Project-2 is a real clone at 0fbd54a, git fetch
works with the deploy key added for that machine, `checkout --force` leaves
.gitignore's .env untouched (it holds DATABASE_URL), and sudo is
passwordless for the three commands.

The skip guard for `server/` stays, because #42 has not merged yet. Once
it does, the next push to main will deploy the API end to end.

Co-Authored-By: opencode <noreply@opencode.ai>
erikhfj added a commit that referenced this pull request Oct 15, 2026
…checks

Four points from rachelks on #45.

1. The searchAlbums smoke query omitted `sort`, which is `AlbumSort!` in
   AlbumSearchInput, so the query failed validation and the check could
   never pass. Now sends `sort: TITLE_ASC`.

2. The skip guards for crate-api.service and server/ now fail instead of
   exiting 0. #42 has merged so both are expected, and a deploy job that
   goes green without deploying anything is the wrong failure mode.

3. The body check parses the JSON rather than grepping for the word
   "errors". Grep would also fire on an album called "Errors", and it did
   not verify `data` was present at all. Now rejects a response with
   errors, with no data, or that is not JSON. Added a totalCount
   assertion as well, so the check is on the data and not only on its
   presence.

4. The python validators live in shell variables on one line. A
   multi-line `python3 -c` heredoc cannot be used inside a YAML block
   scalar: the block ends at the first unindented line and the workflow
   stops parsing. Learned by breaking it.

Also rebased onto main now that #42 and #43 have merged.

Validated the two python expressions against five payloads: a good
response, GraphQL errors, null data, a valid totalCount, and two bad
ones. Each rejected case prints what was wrong.

Co-Authored-By: opencode <noreply@opencode.ai>
@rachelks rachelks deleted the feat/6-graphql-yoga-server-with-searchalbums branch October 16, 2026 18:12
Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GraphQL Yoga server with searchAlbums
2 participants